package org.openarchitectureware.recipe2.workflow;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.mwe.core.WorkflowContext;
import org.eclipse.emf.mwe.core.WorkflowInterruptedException;
import org.eclipse.emf.mwe.core.issues.Issues;
import org.eclipse.emf.mwe.core.lib.AbstractWorkflowComponent2;
import org.eclipse.emf.mwe.core.monitor.ProgressMonitor;
import org.eclipse.internal.xtend.expression.ast.SyntaxElement;
import org.eclipse.xtend.XtendComponent;
import org.eclipse.xtend.expression.EvaluationException;
import org.eclipse.xtend.expression.ExceptionHandler;
import org.eclipse.xtend.expression.ExecutionContext;
import org.eclipse.xtend.expression.ExecutionContextImpl;
import org.eclipse.xtend.expression.TypeSystemImpl;
import org.eclipse.xtend.expression.Variable;
import org.eclipse.xtend.typesystem.MetaModel;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.resource.XtextResourceSet;
import org.openarchitectureware.recipe2.dsl.DetectorStandaloneSetup;
import org.openarchitectureware.recipe2.dsl.RecipeBuilderStandaloneSetup;
import org.openarchitectureware.recipe2.dsl.detector.Detector;
import org.openarchitectureware.recipe2.dsl.detector.DetectorFactory;
import org.openarchitectureware.recipe2.dsl.detector.RecipeDetectorModel;
import org.openarchitectureware.recipe2.dsl.recipeBuilder.PluginImport;
import org.openarchitectureware.recipe2.dsl.recipeBuilder.RecipeBuilderModel;

import com.google.inject.Injector;

public class RecipeGenerator extends AbstractWorkflowComponent2 {

	private static final String COMPONENT_NAME = "Recipe Generator Component";
	public static final String DEFAULT_DETECTORS_SLOT = "recipeDetectors";

	protected final Log log = LogFactory.getLog(getClass());
	private String detectorsSlot = DEFAULT_DETECTORS_SLOT;
	private String detectorModelName = "detectors";
	private final List<MetaModel> metaModels = new ArrayList<MetaModel>();
	protected ExceptionHandler exceptionHandler = null;
	private Object classpathURIContext;
	private String invokeExpression;
	private List<RecipeBuilder> builders = new ArrayList<RecipeBuilder>();
	private RecipeBuilderModel builderModel;
	private String builderModelUri;
	private List<String> pluginJarUriPrefixes = new ArrayList<String>();

	public void addMetaModel(final MetaModel metaModel) {
		assert metaModel != null;
		metaModels.add(metaModel);
	}

	public void addPluginJarUriPrefix(String pluginJarUri) {
		assert pluginJarUri != null;
		this.pluginJarUriPrefixes.add(pluginJarUri);
	}

	public void addRecipeBuilder(final RecipeBuilder builder) {
		assert builder != null;
		this.builders.add(builder);
	}

	@Override
	protected void checkConfigurationInternal(Issues issues) {
	}

	protected RecipeDetectorModel createDetectorModels(WorkflowContext ctx) {
		DetectorStandaloneSetup detectorSetup = new DetectorStandaloneSetup();
		detectorSetup.createInjectorAndDoEMFRegistration();
		final RecipeDetectorModel detectorModel = DetectorFactory.eINSTANCE.createRecipeDetectorModel();
		detectorModel.setName(detectorModelName);
		ctx.set(detectorsSlot, detectorModel);
		return detectorModel;
	}

	protected void generateRecipe(WorkflowContext ctx, ProgressMonitor monitor, Issues issues) {
		// load predef models to be referenced in detectors

		final RecipeDetectorModel detectorModel = createDetectorModels(ctx);

		List<String> pluginJarUris = new ArrayList<String>();
		List<String> builderPluginJarUris = new ArrayList<String>();
		loadPluginJarUris(issues, pluginJarUris, builderPluginJarUris);

		// somehow put pluginJarUris on "classpath" of xtend - they seem to be
		// there when framework projects are open in workspace, but not when
		// loaded as plugin jars normally
		invokeRecipeXtend(ctx, monitor, issues, detectorModel);

		validateDetectorModel(issues, detectorModel);

		runBuilders(ctx, monitor, issues, detectorModel, builderPluginJarUris);
	}

	public RecipeBuilderModel getBuilderModel() {
		if (builderModel == null) {
			builderModel = loadBuilderModel();
		}
		return builderModel;
	}

	public Object getClasspathURIContext() {
		return classpathURIContext;
	}

	@Override
	public String getComponentName() {
		return COMPONENT_NAME;
	}

	/**
	 * @see org.eclipse.emf.mwe.core.lib.AbstractWorkflowComponent#getLogMessage()
	 */
	@Override
	public String getLogMessage() {
		return "Running Recipe Xtend '" + invokeExpression + "'";
	}

	@Override
	protected void invokeInternal(WorkflowContext ctx, ProgressMonitor monitor, final Issues issues) {
		try {
			if (exceptionHandler == null) {
				exceptionHandler = new ExceptionHandler() {
					public void handleRuntimeException(RuntimeException ex, SyntaxElement element, ExecutionContext ctx, Map<String, Object> additionalContextInfo) {
						issues.addError(RecipeGenerator.this, ex.getMessage(), element);
						throw ex;
					}
				};
			}
			generateRecipe(ctx, monitor, issues);
		} catch (EvaluationException e) {
			log.error("Error in Component" + (getId() == null ? " " : " " + getId()) + " of type " + getClass().getName());
			throw new WorkflowInterruptedException(e.getMessage());
		}
	}

	protected void invokeRecipeXtend(WorkflowContext ctx, ProgressMonitor monitor, Issues issues, final RecipeDetectorModel detectorModel) {
		XtendComponent xtend = new XtendComponent() {
			@Override
			protected ExecutionContextImpl getExecutionContext(WorkflowContext ctx) {

				final Map<String, Variable> globalVars = new HashMap<String, Variable>();
				Variable v = new Variable(DEFAULT_DETECTORS_SLOT, detectorModel);
				globalVars.put(v.getName(), v);

				final ExecutionContextImpl executionContext = new ExecutionContextImpl(getResourceManager(), null, new TypeSystemImpl(), new HashMap<String, Variable>(),
						globalVars, null, exceptionHandler, null, getNullEvaluationHandler(), null, callback, null, null);

				for (MetaModel mm : metaModels) {
					executionContext.registerMetaModel(mm);
				}
				return executionContext;
			}
		};

		xtend.setInvoke(invokeExpression);
		for (MetaModel metaModel : metaModels) {
			xtend.addMetaModel(metaModel);
		}
		xtend.setExceptionHandler(exceptionHandler);
		xtend.invoke(ctx, monitor, issues);
	}

	protected RecipeBuilderModel loadBuilderModel() {
		RecipeBuilderStandaloneSetup builderSetup = new RecipeBuilderStandaloneSetup();
		Injector predefInjector = builderSetup.createInjectorAndDoEMFRegistration();
		XtextResourceSet builderResources = predefInjector.getInstance(XtextResourceSet.class);
		builderResources.setClasspathURIContext(getClasspathURIContext());
		builderResources.addLoadOption(XtextResource.OPTION_RESOLVE_ALL, Boolean.TRUE);

		Resource resource = builderResources.getResource(URI.createURI(builderModelUri), true);
		EObject model = resource.getContents().get(0);
		RecipeBuilderModel builderModel = (RecipeBuilderModel) model;

		return builderModel;
	}

	private void loadPluginJarUris(Issues issues, List<String> pluginJarUris, List<String> builderPluginJarUris) {
		for (PluginImport requiredPlugin : getBuilderModel().getPluginImports()) {
			String uri = null;
			for (String pluginJarUriPrefix : pluginJarUriPrefixes) {
				if (uri == null) {
					java.net.URI jarUri = java.net.URI.create(pluginJarUriPrefix + "/" + requiredPlugin.getName()
							+ (requiredPlugin.getVersion() == null ? "" : "_" + requiredPlugin.getVersion()) + ".jar");
					boolean exists = new File(jarUri).exists();
					if (exists) {
						uri = jarUri.toString();
					}
				}
			}
			if (uri != null) {
				pluginJarUris.add(uri);
				if (requiredPlugin.isReExport()) {
					builderPluginJarUris.add(uri);
				}
			} else {
				issues.addError(this, "required plugin not found: " + requiredPlugin.getName());
			}
		}
	}

	private void runBuilders(WorkflowContext ctx, ProgressMonitor monitor, Issues issues, final RecipeDetectorModel detectorModel, List<String> builderPluginJarUris) {
		for (RecipeBuilder builder : builders) {
			builder.setDetectors(detectorModel);
			for (MetaModel metaModel : metaModels) {
				builder.addMetaModel(metaModel);
			}

			for (String pluginJarUri : builderPluginJarUris) {
				builder.addPluginJarUri(pluginJarUri);
			}

			builder.setBuilderModel(getBuilderModel());
			log.info("Recipe Builder - " + builder.getComponentName() + " - " + builder.getLogMessage());
			builder.invoke(ctx, monitor, issues);
		}
	}

	public void setBuilderModelUri(final String uri) {
		builderModelUri = uri;
	}

	public void setClasspathURIContext(Object classpathURIContext) {
		this.classpathURIContext = classpathURIContext;
	}

	public void setDetectorModelName(String detectorModelName) {
		this.detectorModelName = detectorModelName;
	}

	public void setDetectorsSlot(String outputSlot) {
		this.detectorsSlot = outputSlot;
	}

	public void setExceptionHandler(final ExceptionHandler exceptionHandler) {
		this.exceptionHandler = exceptionHandler;
	}

	/**
	 * Sets the invoke expression.
	 * 
	 * @param invokeExpr
	 *            the invoke expression
	 */
	public void setInvoke(final String invokeExpr) {
		invokeExpression = invokeExpr;
	}

	private void validateDetectorModel(Issues issues, final RecipeDetectorModel detectorModel) {
		boolean validDetectorModel = true;
		for (Detector d : detectorModel.getBugDetectors()) {
			if (d.getPredef() == null) {
				validDetectorModel = false;
				issues.addError(this, "Detector created without predef", d);
			}
		}
		if (!validDetectorModel) {
			throw new WorkflowInterruptedException("Invalid Detector Model");
		}
	}

}
